unit UltimateSheduler_Actions;

interface

uses
  DMPluginsSDK,
  UltimateSheduler_Defines,
  Classes;

type
  TActionThread = class(TThread)
  private
    { Private declarations }
    FPlugin: TDMPluginBase2;
    aTask: Task;
    aRule, doRule: Rule;
    anAG: ActionGroup;
    aAction, doAction: anAction;
    pdIndex: Integer;
    function DoDMAction(const Cmd, Params: String; Core: Boolean = false): String;
    procedure FileMapping(Sender: TObject; const OldFileName, NewFileName: string);
    function Launch: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create(APlugIn: TDMPluginBase2);
    property PlugIn:TDMPluginBase2 read FPlugin;
  end;

procedure TATStart(APlugIn: TDMPluginBase2);
procedure TATResume;
procedure TATFinish;
//
{
task info encoded as
<TaskTitle>[<TitleSep><DownloadID>][#13#10]
}
procedure AddTasks(AInfo: String);

implementation

uses
  gnugettext,
  Locales,
  Windows,
  ShellAPI,
  SysUtils,
  StrUtils,
  SyncObjs,
  Tokens,
  StringUtils,
  LogFile,
  JvSHFileOperation,
  DMDownloads,
  PluginEx_Tokens,
  ServiceID,
  ServiceFilter_Tokens;

const
 tknPath = '%P';
 tknFolder = '%F';
 tknName = '%N';

var
 TAT: TActionThread = nil;
 ProcessData: TProcesses = nil;
 FileOps: TJvSHFileOperation = nil;
 CS_Process: TCriticalSection = nil;

procedure TATStart(APlugIn: TDMPluginBase2);
begin
 TATFinish;
 CS_Process:=TCriticalSection.Create;
 FileOps:=TJvSHFileOperation.Create(nil);
 ProcessData:=TProcesses.Create(Process);
 TAT:=TActionThread.Create(APlugIn);
 FileOps.OnFileMapping:=TAT.FileMapping;
end;

procedure TATResume;
begin
 if Assigned(TAT) then
  begin
   if TAT.Suspended then
     TAT.Resume;
  end;
end;

procedure TATFinish;
begin
 if Assigned(TAT) then
  begin
   TAT.Terminate;
   if not TAT.Suspended then
     TAT.WaitFor;
   FreeAndNil(TAT);
  end;
 FreeAndNil(ProcessData);
 FreeAndNil(FileOps);
 FreeAndNil(CS_Process);
end;

procedure AddTasks(AInfo: String);
 var
  i, t, j, k: Integer;
  s, at, rt, tt, di: String;
begin
 CS_Process.Enter;
 for i:=1 to WordCount(AInfo, [#13, #10]) do
  begin
   //get task line
   s:=ExtractWord(i, AInfo, [#13, #10]);
   //get titles and id
   tt:=ExtractWord(1, s, [TitleSep]);
   di:=ExtractWord(2, s, [TitleSep]);
   if tt='' then //no task title specified
     Continue;
   //get task id
   CS_Tasks.Enter;
   j:=mySettings.Tasks.IndexOfTitle(tt);
   CS_Tasks.Leave;
   if j>=0 then
    begin
     //add new process
     t:=ProcessData.Add.Index;
     with ProcessData[t] do
      begin
       //pre-setup
       CS_Tasks.Enter;
       TaskID:=mySettings.Tasks[j].ID;
       CS_Tasks.Leave;
       Info:=di;
       {pre-setup in Process.Create
       RuleID:=ridNoRule;
       ActionGroupID:=ridNoRule;
       ActionID:=aidNoAction;
       }
       //Done:=dNotStarted;
       //find rule
       CS_Tasks.Enter;
       rt:=mySettings.Tasks[j].RuleTitle;
       CS_Tasks.Leave;
       if rt<>'' then
        begin
         //set rule id
         CS_Rules.Enter;
         k:=mySettings.Rules.IndexOfTitle(rt);
         if k>=0 then
           RuleID:=mySettings.Rules[k].ID;
         CS_Rules.Leave;
        end;
       //find action group
       CS_Tasks.Enter;
       at:=mySettings.Tasks[j].ActionsTitle;
       CS_Tasks.Leave;
       if at='' then
         ActionGroupID:=idComplete //some error in task - mark process as completed
       else
        begin
         CS_Actions.Enter;
         k:=mySettings.ActionGroups.IndexOfTitle(at);
         if k>=0 then
           ActionGroupID:=mySettings.ActionGroups[k].ID;
         CS_Actions.Leave;
        end;
      end;
     //check for errors
     if (ProcessData[t].TaskID<0) or (ProcessData[t].ActionGroupID<0) then
       ProcessData.Delete(t)
     else
       Log(logTask, Format(logTaskAdded, [tt, IfThen(rt='', _('None'), rt), at]));
    end;
  end;
 CS_Process.Leave;
 //launch processing
 if Assigned(TAT) then
  begin
   if TAT.Suspended then
    begin
     Log(logThread, 'Thread resumed.');
     TAT.Resume;
    end;
  end;
end;

procedure CreateSemaphore(Filename: String);
 var
  FileHandle: Integer;
begin
 try
  FileHandle := FileCreate(Filename);
 except
  on E: Exception do
   begin
    //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
     begin
      Log(logError, Format(_('Cannot create semaphore %s: error "%s"'), [Filename, E.Message]));
     end;
    FileHandle:=0;
   end;
 end;
 FileClose(FileHandle);
end;

function MAKELANGID(p, s: Integer): Integer;
begin
 Result:=(WORD(s) shl 10) or WORD(p);
end;

{ TActionThread }

constructor TActionThread.Create(APlugIn: TDMPluginBase2);
begin
 inherited Create(true);
 Priority:=tpLower;
 FPlugin:=APlugIn;
 aTask:=nil;
 aRule:=nil;
 anAG:=nil;
 doAction:=nil;
 pdIndex:=-1;
end;

function TActionThread.DoDMAction(const Cmd, Params: String; Core: Boolean = false): String;
begin
 if Assigned(FPlugin) then
  begin
   if Core then
     Result:=Plugin.CoreAPI.DoAction(Cmd, Params)
   else
     Result:=Plugin.DoAction(Cmd, Params);
  end
 else
   Result:='';
end;

function TActionThread.Launch: Integer;
 var
  FF, FP, FN, P: String;
  emsg: PChar;
begin
 Result:=idFindNext; //mark launch as done
 //
 if not FileExists(doAction.Destination) then
  begin
   //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
    begin
     Log(logError, Format(_('File %s is not found, nothing to launch.'), [doAction.Destination]));
    end;
   //
   Exit;
  end;
 //
 if (pdIndex>=0) and (pdIndex<ProcessData.Count) then
  begin
   FF:=ExtractToken('saveto', ProcessData[pdIndex].Info);
   if FileExists(FF) then
    begin
     FP:=IncludeTrailingPathDelimiter(ExtractFilePath(FF));
     FN:=ExtractFileName(FF);
     P:=doAction.Parameter;
     ReplaceEx(P, tknPath, FF);
     ReplaceEx(P, tknFolder, FP);
     ReplaceEx(P, tknName, FN);
     if doAction.Flag then
      begin
       //fill process info
       ProcessData[pdIndex].SEI_Clear;
       with ProcessData[pdIndex].SEI do
        begin
         cbSize:=SizeOf(TShellExecuteInfo);
         fMask := SEE_MASK_NOCLOSEPROCESS;
         Wnd   := 0;  // forms handle
         lpVerb:= 'open';
         lpFile:= PChar(doAction.Destination);
         lpParameters := PChar(P);
         nShow := SW_SHOWMINNOACTIVE;
        end;
       //
       if ShellExecuteEx(@ProcessData[pdIndex].SEI) then
         Result:=idWaitForProcess
       else
        begin
         ProcessData[pdIndex].SEI_Clear;
         //
         //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
          begin
           emsg:=nil;
           FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM, nil, GetLastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), emsg, 0, nil);
           Log(logError, Format(_('Cannot launch file %s: %s.'), [doAction.Destination, emsg]));
           LocalFree(Cardinal(emsg));
          end;
        end;
      end
     else
      begin
        case ShellExecute(0, 'open', PChar(doAction.Destination), PChar(P), nil, SW_SHOWMINNOACTIVE) of
         ERROR_BAD_FORMAT:
          begin
           //MessageBox(0, PChar(ra.Path+#13+seCannotStart), PChar(myPluginName), MB_OK or MB_TOPMOST or MB_ICONERROR);
           //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
            begin
             Log(logError, Format(_('File %s cannot be launched.'), [doAction.Destination]));
            end;
          end;
         ERROR_FILE_NOT_FOUND:
          begin
           //MessageBox(0, PChar(ra.Path+#13+seInvalidFile), PChar(myPluginName), MB_OK or MB_TOPMOST or MB_ICONERROR);
           //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
            begin
             Log(logError, Format(_('File %s is not found.'), [doAction.Destination]));
            end;
          end;
         ERROR_PATH_NOT_FOUND:
          begin
           //MessageBox(0, PChar(ra.Path+#13+seInvalidFolder), PChar(myPluginName), MB_OK or MB_TOPMOST or MB_ICONERROR);
           //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
            begin
             Log(logError, Format(_('Path to file %s is not found.'), [doAction.Destination]));
            end;
          end;
         end;
      end;
    end
   else
    begin
     //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
      begin
       Log(logError, Format(_('File %s is not found.'), [FF]));
      end;
    end;
  end;
end;

procedure TActionThread.FileMapping(Sender: TObject; const OldFileName, NewFileName: string);
 var
  s: WideString;
begin
 if Assigned(aRule) and Assigned(doAction) then
  begin
   //if IsBitsSet(myRules.Log, [cLogUse, cLogActions]) then
    begin
     Log(logAction, Format(_('File "%s" was renamed to "%s" while performing action "%s %s" in rule %s.'), [OldFileName, NewFileName, GetActTypeDesc(doAction.ActType), doAction.Destination, aRule.Title]));
    end;
   //
   if doAction.ActType in [actCopy, actMove, actRename] then
    begin
     if doAction.Flag and (pdIndex>=0) then
      begin
       //update file info on collision
       if pdIndex<ProcessData.Count then
        begin
         s:=ProcessData[pdIndex].Info;
         ReplaceToken('saveto', NewFileName, s);
         ProcessData[pdIndex].Info:=s;
         //
         //if IsBitsSet(myRules.Log, [cLogUse, cLogActions]) then
          begin
           Log(logAction, _('Download info is updated.'));
          end;
        end;
      end;
    end;
  end;
end;

procedure TActionThread.Execute;
 var
  i, t: Integer;
  fn, newfn, oldfn, Param: String;
  ws: WideString;
  AOptions: TJvShFileOptions;
  CanLaunch: Boolean;
 procedure MarkProcessToStop(aProcess: Process);
 begin
  aProcess.ActionID:=idComplete;
  aProcess.ActionGroupID:=idComplete;
  aProcess.RuleID:=idComplete;
 end;
begin
 //pre-init
 doAction:=anAction.Create(nil);
 Log(logThread, 'Thread started.');
 //cycle
 while not Terminated do
  begin
   i:=0;
   while (not Terminated) and (i<ProcessData.Count) do
    begin
     CS_Process.Enter;
     //init
     aTask:=nil;
     aRule:=nil;
     anAG:=nil;
     aAction:=nil;
     fn:='';
     //get task
     CS_Tasks.Enter;
     aTask:=Task(mySettings.Tasks.FindItemID(ProcessData[i].TaskID));
     CS_Tasks.Leave;
     //check state of process
     if ProcessData[i].ActionGroupID<0 then
      begin
       //delete process
       if Assigned(aTask) then
         Log(logAction, Format(_('Task "%s" is done.'), [aTask.Title]))
       else
         Log(logAction, _('Task was dropped when done.'));
       ProcessData.Delete(i);
      end
     else
      begin
       //check rule if specified
       if ProcessData[i].RuleID>idComplete then
        begin
         //find rule
         CS_Rules.Enter;
         aRule:=Rule(mySettings.Rules.FindItemID(ProcessData[i].RuleID));
         CS_Rules.Leave;
         //
         if Assigned(aRule) then
          begin
           doRule:=Rule.Create(nil);
           CS_Rules.Enter;
           doRule.Assign(aRule);
           CS_Rules.Leave;
           if doRule.Matched(ProcessData[i].Info) then
            begin
             if Assigned(aTask) then
               Log(logAction, Format(_('Task "%s": rule is matched.'), [aTask.Title]))
             else
               Log(logError, _('Task was dropped when rule matched.'));
            end
           else
            begin
             //if rule does not match - delete task
             ProcessData[i].ActionGroupID:=idComplete;
             if Assigned(aTask) then
               Log(logAction, Format(_('Task "%s": rule does not match. Task is skipped.'), [aTask.Title]))
             else
               Log(logError, _('Task was dropped and rule does not match.'));
            end;
           FreeAndNil(doRule);
          end;
         //mark rule matched
         ProcessData[i].RuleID:=idFindFirst;
        end;
       //perform actions
       case ProcessData[i].ActionID of
        idComplete:
         begin
          //process completed all actions
          ProcessData[i].ActionGroupID:=idComplete;
         end;
        idFindFirst:
         begin
          //setup for first action
          CS_Actions.Enter;
          anAG:=ActionGroup(mySettings.ActionGroups.FindItemID(ProcessData[i].ActionGroupID));
          CS_Actions.Leave;
          if Assigned(anAG) then
           begin
            CS_Actions.Enter;
            if anAG.Actions.Count>0 then
             begin
              ProcessData[i].ActionID:=anAG.Actions[0].ID;
              //get download info if id specified
              if ProcessData[i].Info<>'' then
               begin
                t:=StrToIntDef(ProcessData[i].Info, 0);
                if t>0 then
                  ProcessData[i].Info:=DoDMAction('GetDownloadInfoByID', ProcessData[i].Info, true);
               end;
             end
            else
             begin
              if Assigned(aTask) then
                Log(logError, Format(_('Task "%s": action group "%s" has no actions. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]))
              else
                Log(logError, _('Task was dropped when action group has no actions.'));
              //mark process as completed
              ProcessData[i].ActionGroupID:=idComplete;
             end;
            CS_Actions.Leave;
           end
          else
           begin
            if Assigned(aTask) then
              Log(logError, Format(_('Task "%s": action group "%s" is not exists. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]))
            else
              Log(logError, _('Task was dropped when action group is not exists.'));
            //mark process as completed
            ProcessData[i].ActionGroupID:=idComplete;
           end;
         end;
        else
         begin
          //get action data
          CS_Actions.Enter;
          anAG:=ActionGroup(mySettings.ActionGroups.FindItemID(ProcessData[i].ActionGroupID));
          if Assigned(anAG) then
            aAction:=anAction(anAG.Actions.FindItemID(ProcessData[i].ActionID));
          CS_Actions.Leave;
          if Assigned(anAG) and Assigned(aAction) then
           begin
            doAction.Assign(aAction);
            //check action state
            case ProcessData[i].RuleID of
             idComplete:
              begin
               //action is completed
               ProcessData[i].ActionID:=idComplete;
              end;
             idFindFirst:
              begin
               CanLaunch:=false;
               //check for file name existance
               if doAction.ActType in [actCopy, actMove, actLaunch, actRename] then
                begin
                 fn:=ExtractToken('saveto', ProcessData[i].Info);
                 if fn='' then
                  begin
                   //some error occured
                   if Assigned(aTask) then
                    begin
                     Log(logError, Format(_('Task "%s": file name dropped in action "%s". Action is skipped.'), [aTask.Title, GetActTypeDesc(doAction.ActType)]));
                     //skip action to next
                     ProcessData[i].RuleID:=idFindNext;
                    end
                   else
                    begin
                     Log(logError, Format(_('Task dropped when file name dropped in action "%s". Task is skipped.'), [GetActTypeDesc(doAction.ActType)]));
                     //mark task as completed
                     MarkProcessToStop(ProcessData[i]);
                    end;
                  end
                 else
                   CanLaunch:=true;
                end
               else
                 CanLaunch:=true;
               //
               if CanLaunch then
                begin
                 //launch action
                 case doAction.ActType of
                  //copy or move
                  actCopy, actMove, actRename:
                   begin
                    pdIndex:=i; //save Process index for data changing
                    AOptions := [fofFilesOnly, fofNoConfirmMkDir, fofWantMappingHandle];
                    if mySettings.Silent then
                      Include(AOptions, fofSilent);
                    case doAction.Existing of
                     actexRename: Include(AOptions, fofRenameOnCollision);
                     actexOverwrite: Include(AOptions, fofNoConfirmation);
                     end;
                    FileOps.SourceFiles.Clear;
                    FileOps.SourceFiles.Add(fn);
                    if doAction.ActType=actRename then
                      //newfn is old path and new name
                      newfn:=IncludeTrailingPathDelimiter(ExtractFilePath(fn))+doAction.Destination
                    else
                      //newfn is new path
                      newfn:=IncludeTrailingPathDelimiter(doAction.Destination);
                    FileOps.DestFiles.Clear;
                    FileOps.DestFiles.Add(newfn);
                    if doAction.ActType=actCopy then
                      FileOps.Operation:=foCopy
                    else
                      FileOps.Operation:=foMove;
                    FileOps.Options:=AOptions;
                    try
                     FileOps.Execute;
                     if doAction.Flag then
                      begin
                       //look for name collision
                       oldfn:=ExtractToken('saveto', ProcessData[i].Info);
                       if oldfn=fn then
                        begin
                         //was no collision - update info
                         ws:=ProcessData[i].Info;
                         if doAction.ActType=actRename then
                           //save new filename
                           ReplaceToken('saveto', newfn, ws)
                         else
                           //save only new path
                           ReplaceToken('saveto', newfn+ExtractFileName(oldfn), ws);
                         ProcessData[i].Info:=ws;
                        end;
                      end;
                    except
                     on E: Exception do
                      begin
                       //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
                        begin
                         case doAction.ActType of
                          actCopy: Param:=_('copy');
                          actMove: Param:=_('move');
                          actRename: Param:=_('rename');
                          end;
                         Log(logError, Format(_('Cannot %s file %s to %s: error "%s"'), [Param, fn, newfn, E.Message]));
                        end;
                      end;
                    end;
                    //goto next action
                    ProcessData[i].RuleID:=idFindNext;
                   end;
                  //launch program
                  actLaunch:
                   begin
                    //if IsBitsSet(myRules.Log, [cLogUse, cLogActions]) then
                     begin
                      Log(logAction, Format(_('Action "%s %s" is performing with file %s.'), [GetActTypeDesc(doAction.ActType), doAction.Destination, fn]));
                     end;
                    //
                    pdIndex:=i;
                    ProcessData[i].RuleID:=Launch;
                   end;
                  //start downloads
                  actStart:
                   begin
                    //if IsBitsSet(myRules.Log, [cLogUse, cLogActions]) then
                     begin
                      Log(logAction, Format(_('Action "%s %s" is performing with file %s.'), [GetActTypeDesc(doAction.ActType), doAction.Destination, fn]));
                     end;
                    //
                    if doAction.Flag then
                     begin
                      //find first download of category - Service: Filter required!
                      if StrToBoolDef(DoDMAction(tknIsPluginActive, ServiceFilter), false) then
                       begin
                        Param:=EncodeToken(sdiState, IntToStr(dsPause));
                        Param:=Param+EncodeToken(sdiCategory, CatList.Names[IndexOfValue(doAction.Destination, CatList)]);
                        ws:=DoDMAction(tknFilter, Param);
                        if ws='' then
                         begin
                          Log(logError, Format(_('Category %s has no paused downloads.'), [doAction.Destination]));
                         end
                        else
                         begin
                          Param:=ExtractWord(1, ws, [' ']);
                          DoDMAction('StartDownloads', Param, true);
                          Log(logAction, Format(_('Category %s started download %s.'), [doAction.Destination, Param]));
                         end;
                        ProcessData[i].RuleID:=idFindNext;
                       end
                      else
                       begin
                        Log(logError, Format(_('Can not find downloads in category %s - Service: Filter is required!'), [doAction.Destination]));
                       end;
                     end
                    else
                     begin
                      DoDMAction('StartNode', CatList.Names[IndexOfValue(doAction.Destination, CatList)]);
                      Log(logAction, Format(_('Category %s is started.'), [doAction.Destination]));
                      ProcessData[i].RuleID:=idFindNext;
                     end;
                   end;
                  //create flag
                  actFlagCreate:
                   begin
                    //if IsBitsSet(myRules.Log, [cLogUse, cLogActions]) then
                     begin
                      Log(logAction, Format(_('Action "%s %s" is performing with file %s.'), [GetActTypeDesc(doAction.ActType), doAction.Destination, fn]));
                     end;
                    //
                    CreateSemaphore(doAction.Destination);
                    ProcessData[i].RuleID:=idFindNext;
                   end;
                  //delete flag
                  actFlagDelete:
                   begin
                    //if IsBitsSet(myRules.Log, [cLogUse, cLogActions]) then
                     begin
                      Log(logAction, Format(_('Action "%s %s" is performing with file %s.'), [GetActTypeDesc(doAction.ActType), doAction.Destination, fn]));
                     end;
                    //
                    AOptions := [fofFilesOnly, fofNoConfirmation];
                    if mySettings.Silent then
                      Include(AOptions, fofSilent);
                    {
                    case doAction.Existing of
                     actexRename: Include(AOptions, fofRenameOnCollision);
                     actexOverwrite: Include(AOptions, fofNoConfirmation);
                     end;
                    }
                    FileOps.SourceFiles.Clear;
                    FileOps.SourceFiles.Add(doAction.Destination);
                    FileOps.Operation:=foDelete;
                    FileOps.Options:=AOptions;
                    try
                     FileOps.Execute;
                    except
                     on E: Exception do
                      begin
                       //if IsBitsSet(myRules.Log, [cLogUse, cLogActionErrors]) then
                        begin
                         Log(logError, Format(_('Cannot delete semaphore %s: error "%s"'), [doAction.Destination, E.Message]));
                        end;
                      end;
                    end;
                    ProcessData[i].RuleID:=idFindNext;
                   end;
                  //
                  else
                   begin
                    //some error occured
                    if Assigned(aTask) then
                     begin
                      if Assigned(anAG) then
                       begin
                        Log(logError, Format(_('Task "%s": action group "%s" has unknown type %d of action with ID %d. Action is skipped.'), [aTask.Title, aTask.ActionsTitle, doAction.ActType, ProcessData[i].ActionID]));
                        //skip action to next
                        ProcessData[i].RuleID:=idFindNext;
                       end
                      else
                       begin
                        Log(logError, Format(_('Task "%s": action group "%s" is not exists. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]));
                        MarkProcessToStop(ProcessData[i]);
                       end;
                     end
                    else
                     begin
                      Log(logError, _('Task dropped when performing action of unknown type.'));
                      MarkProcessToStop(ProcessData[i]);
                     end;
                   end;
                  end;
                end;
              end;
             idFindNext:
              begin
               //find next action
               CS_Actions.Enter;
               t:=aAction.Index+1;
               if t<anAG.Actions.Count then
                begin
                 //take next action
                 ProcessData[i].ActionID:=anAG.Actions[t].ID;
                 ProcessData[i].RuleID:=idFindFirst;
                end
               else
                begin
                 //no more actions
                 ProcessData[i].ActionID:=idComplete;
                end;
               CS_Actions.Leave;
              end;
             idWaitForFile:
              begin
               //check for file existance
               fn:=ExtractToken('saveto', ProcessData[i].Info);
               if fn='' then
                begin
                 //some error occured
                 if Assigned(aTask) then
                   Log(logError, Format(_('Task "%s": file name dropped. Task is skipped.'), [aTask.Title]))
                 else
                   Log(logError, _('Task dropped when file name dropped. Task is skipped.'));
                 //mark task as completed
                 MarkProcessToStop(ProcessData[i]);
                end
               else
                begin
                 if FileExists(fn) then
                  begin
                   //prepare to launch
                   ProcessData[i].RuleID:=idFindFirst;
                   //if IsBitsSet(myRules.Log, [cLogUse, cLogFileSaved]) then
                    begin
                     Log(logFiles, Format(_('File %s was saved.'), [fn]));
                     {
                     if (myRules.LogDump=cTimeEvery) and (LogFolder<>'') then
                       LogSave(LogFolder+LogFilename);
                     }
                    end;
                  end;
                end;
              end;
             idWaitForProcess:
              begin
               //check state of external process
               if not IsProcessActive(ProcessData[i].SEI) then
                begin
                 //prepare to next action
                 ProcessData[i].SEI_Clear;
                 ProcessData[i].RuleID:=idFindNext;
                end;
              end;
             else
              begin
               //some error occured
               if Assigned(aTask) then
                 Log(logError, Format(_('Task "%s": incorrect action state. Task is skipped.'), [aTask.Title]))
               else
                 Log(logError, 'Task dropped when incorrect action state. Task is skipped.');
               //mark task as completed
               MarkProcessToStop(ProcessData[i]);
              end;
             end;
           end
          else
           begin
            //some error occured
            if Assigned(aTask) then
             begin
              if Assigned(anAG) then
                Log(logError, Format(_('Task "%s": action group "%s" dropped action with ID %d. Task is skipped.'), [aTask.Title, aTask.ActionsTitle, ProcessData[i].ActionID]))
              else
                Log(logError, Format(_('Task "%s": action group "%s" is not exists. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]));
             end
            else
             begin
              Log(logError, _('Task dropped when action group is not exists.'));
             end;
            MarkProcessToStop(ProcessData[i]);
           end;
         end;
        end;
       //go to next process
       Inc(i);
      end;
     (*
     case ProcessData[i].Done of
      dDone: //mark completed process to delete
        //ProcessData[i].TaskID:=ridDelete;
        ProcessData[i].ActionGroupID:=idComplete;
      dNotStarted:
       begin
        //mark task as launched
        CS_Tasks.Enter;
        aTask:=Task(mySettings.Tasks.FindItemID(ProcessData[i].TaskID));
        if Assigned(aTask) then
         begin
          aTask.LastLaunch:=Now;
          Log(logAction, Format(_('Task "%s" is launched.'), [aTask.Title]));
         end;
        CS_Tasks.Leave;
        //get download info if id specified
        if ProcessData[i].Info<>'' then
         begin
         t:=StrToIntDef(ProcessData[i].Info, 0);
         if t>0 then
           ProcessData[i].Info:=DoDMAction('GetDownloadInfoByID', ProcessData[i].Info, true);
         end;
        //mark process as started
        ProcessData[i].Done:=dContinue;
       end;
      end;
     //check task
     CS_Tasks.Enter;
     aTask:=Task(mySettings.Tasks.FindItemID(ProcessData[i].TaskID));
     CS_Tasks.Leave;
     if ProcessData[i].ActionGroupID<0 then
      begin
       //delete process
       if Assigned(aTask) then
         Log(logAction, Format(_('Task "%s" is done.'), [aTask.Title]));
       ProcessData.Delete(i);
      end
     else
      begin
       //check rule if specified
       //if ProcessData[i].RuleID>ridNoRule then
       if ProcessData[i].RuleID>idComplete then
        begin
         //find rule
         CS_Rules.Enter;
         aRule:=Rule(mySettings.Rules.FindItemID(ProcessData[i].RuleID));
         CS_Rules.Leave;
         //
         if aRule.Matched(ProcessData[i].Info) then
          begin
           if Assigned(aTask) then
             Log(logAction, Format(_('Task "%s": rule is matched.'), [aTask.Title]))
           else
             Log(logError, 'Task dropped when rule matched.');
          end
         else
          begin
           //if rule does not match - delete task
           ProcessData[i].Done:=dDone;
           if Assigned(aTask) then
             Log(logAction, Format(_('Task "%s": rule does not match. Task is skipped.'), [aTask.Title]))
           else
             Log(logError, 'Task dropped and rule does not match.');
          end;
         //mark rule matched
         ProcessData[i].RuleID:=ridNoRule;
        end;
       //perform actions
       if ProcessData[i].Done>dDone then
        begin
         case ProcessData[i].ActionID of
          aidComplete:
           begin
            //processed and ready to delete
            ProcessData[i].Done:=dDone;
            ProcessData[i].ActionID:=aidNoAction;
           end;
          aidNoAction:
           begin
            //setup for first action
            CS_Actions.Enter;
            anAG:=ActionGroup(mySettings.ActionGroups.FindItemID(ProcessData[i].ActionGroupID));
            CS_Actions.Leave;
            if Assigned(anAG) then
             begin
              CS_Actions.Enter;
              if anAG.Actions.Count>0 then
                ProcessData[i].ActionID:=anAG.Actions[0].ID
              else
               begin
                ProcessData[i].Done:=dDone;
                if Assigned(aTask) then
                  Log(logError, Format(_('Task "%s": action group "%s" has no actions. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]))
                else
                  Log(logError, 'Task dropped when action group has no actions.');
               end;
              CS_Actions.Leave;
             end
            else
             begin
              ProcessData[i].Done:=dDone;
              if Assigned(aTask) then
                Log(logError, Format(_('Task "%s": action group "%s" is not exists. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]))
              else
                Log(logError, 'Task dropped when action group is not exists.');
             end;
           end;
          else
           begin
            //have action, do it
            CS_Actions.Enter;
            anAG:=ActionGroup(mySettings.ActionGroups.FindItemID(ProcessData[i].ActionGroupID));
            if Assigned(anAG) then
              doAction:=anAction(anAG.Actions.FindItemID(ProcessData[i].ActionID));
            CS_Actions.Leave;
            if Assigned(anAG) and Assigned(doAction) then
             begin
              //
             end
            else
             begin
              if Assigned(aTask) then
               begin
                if Assigned(anAG) then
                  Log(logError, Format(_('Task "%s": action group "%s" dropped action with ID %d. Task is skipped.'), [aTask.Title, aTask.ActionsTitle, ProcessData[i].ActionID]))
                else
                  Log(logError, Format(_('Task "%s": action group "%s" is not exists. Task is skipped.'), [aTask.Title, aTask.ActionsTitle]));
               end
              else
               begin
                Log(logError, 'Task dropped when action group is not exists.');
               end;
             end;
           end;
          end;
         {
         }
        end;
       //go to next process
       Inc(i);
      end;
     *)
     //
     CS_Process.Leave;
     Sleep(100); //take a rest ;)
    end;
   if ProcessData.Count=0 then
    begin
     Log(logThread, 'Thread suspended.');
     Suspend;
    end;
  end;
 Log(logAction, 'Thread stopped.');
 //free all
 aTask:=nil;
 aRule:=nil;
 anAG:=nil;
 if Assigned(doAction) then
   FreeAndNil(doAction);
end;

end.
